package com.relax.mybatis.encrypt.util;

import java.lang.reflect.Field;
import java.lang.reflect.Proxy;
import java.util.List;
import java.util.Map;
import java.util.function.BiConsumer;

import org.apache.ibatis.executor.resultset.DefaultResultSetHandler;
import org.apache.ibatis.mapping.MappedStatement;
import org.apache.ibatis.plugin.Invocation;
import org.apache.ibatis.plugin.Plugin;
import org.apache.ibatis.reflection.MetaObject;
import org.apache.ibatis.session.Configuration;

import com.relax.mybatis.annotation.EncryptField;
import com.relax.mybatis.constant.Algorithm;
import com.relax.mybatis.encrypt.config.EncryptProperties;
import com.relax.mybatis.encrypt.strategy.EncryptContext;
import com.relax.mybatis.encrypt.strategy.EncryptFieldHolder;
import com.relax.mybatis.model.FieldCacheProperty;
import com.relax.mybatis.util.PluginUtil;

import lombok.extern.slf4j.Slf4j;

/**
 * @author relax
 * @date 2022/07/05
 * @desc 加密工具类
 */
@Slf4j
public class EncryptUtil {

    public static Object handleExecutor(Invocation invocation,
                                        BiConsumer<MetaObject, FieldCacheProperty> consumer) throws Throwable {
        Object[] args = invocation.getArgs();
        MappedStatement mappedStatement = (MappedStatement) args[0];
        if (PluginUtil.isWriteCommand(mappedStatement.getSqlCommandType())) {
            Configuration configuration = mappedStatement.getConfiguration();
            Object object = args[1];
            if (null != object) {
                if (object instanceof Map) {
                    Map map = (Map) object;
                    for (Object key : map.keySet()) {
                        Object value = map.get(key);
                        if (null != value) {
                            if (value instanceof java.util.List) {
                                if (((String) key).startsWith("param"))
                                    continue;
                                List list = (List) value;
                                for (Object item : list) {
                                    if (EncryptFieldHolder.handleObject(configuration, item, consumer))
                                        ;
                                }
                                continue;
                            }
                            if (EncryptFieldHolder.handleObject(configuration, value, consumer))
                                break;
                        }
                    }
                } else {
                    EncryptFieldHolder.handleObject(configuration, object, consumer);
                }
            }
        }
        return invocation.proceed();
    }

    /**
     * 加密
     *
     * @param encryptProperties
     * @param metaObject
     * @param fieldCacheProperty
     */
    public static void doEecrypt(EncryptProperties encryptProperties,
                                 MetaObject metaObject,
                                 FieldCacheProperty fieldCacheProperty) {
        String name = fieldCacheProperty.getFieldName();
        EncryptField encryptField = fieldCacheProperty.getEncryptField();
        if (null != encryptField) {
            Object object = metaObject.getValue(name);
            if (null != object && object instanceof String) {
                try {
                    Algorithm algorithm = encryptProperties.getAlgorithm(encryptField);
                    String password = encryptProperties.getPassword(encryptField);
                    object = EncryptContext.getStrategy(algorithm).encrypt(algorithm, password,
                            encryptProperties.getPublicKey(), (String) object);
                } catch (Exception e) {
                    log.error("handle encrypt field {}.{} occur exception: {}",
                            metaObject.getOriginalObject().getClass().getTypeName(), name, e.getMessage());
                }
                metaObject.setValue(name, object);
            }
        }
    }

    public static Object handleResultSet(Invocation invocation,
                                         BiConsumer<MetaObject, FieldCacheProperty> consumer) throws Throwable {
        List<Object> list = (List<Object>) invocation.proceed();
        if (list.isEmpty())
            return list;
        Object target = invocation.getTarget();
        while (target instanceof Proxy) {
            Plugin plugin = (Plugin) Proxy.getInvocationHandler(target);
            Field field = plugin.getClass().getDeclaredField("target");
            field.setAccessible(true);
            target = field.get(plugin);
        }
        if (target instanceof DefaultResultSetHandler) {
            DefaultResultSetHandler defaultResultSetHandler = (DefaultResultSetHandler) target;
            Field field = defaultResultSetHandler.getClass().getDeclaredField("mappedStatement");
            field.setAccessible(true);
            MappedStatement mappedStatement = (MappedStatement) field.get(defaultResultSetHandler);
            Configuration configuration = mappedStatement.getConfiguration();
            for (Object object : list) {
                if (null != object && !EncryptFieldHolder.handleObject(configuration, object, consumer))
                    break;
            }
        }
        return list;
    }

    /**
     * 解密
     *
     * @param encryptProperties
     * @param metaObject
     * @param fieldCacheProperty
     */
    public static void doDecrypt(EncryptProperties encryptProperties,
                                 MetaObject metaObject,
                                 FieldCacheProperty fieldCacheProperty) {
        String name = fieldCacheProperty.getFieldName();
        EncryptField encryptField = fieldCacheProperty.getEncryptField();
        if (null != encryptField) {
            Object object = metaObject.getValue(name);
            if (null != object && object instanceof String) {
                try {
                    Algorithm algorithm = encryptProperties.getAlgorithm(encryptField);
                    String password = encryptProperties.getPassword(encryptField);
                    object = EncryptContext.getStrategy(algorithm).decrypt(algorithm, password,
                            encryptProperties.getPrivateKey(), (String) object);
                } catch (Exception e) {
                    log.error("handle decrypt field {}.{} occur exception: {}",
                            metaObject.getOriginalObject().getClass().getTypeName(), name, e.getMessage());
                }
                metaObject.setValue(name, object);
            }
        }
    }

}
